/*
 * Copyright 2017, Data61, CSIRO (ABN 41 687 119 230)
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <autoconf.h>

#include <assert.h>
#include <stdio.h>
#include <stdbool.h>
#include <stdio.h>
#include <errno.h>
#include <sel4/sel4.h>
#include <sel4/arch/constants.h>
#include <camkes.h>
#include <camkes/irq.h>
#include <platsupport/time_manager.h>
#include <platsupport/local_time_manager.h>
#include <platsupport/irq.h>
#include <utils/util.h>
#include <sel4utils/sel4_zf_logif.h>
#include <simple/simple.h>
#include <camkes/io.h>

#include "plat.h"

/* ltimer for accessing timer devices */
static ltimer_t ltimer;
/* time manager for timeout multiplexing */
static time_manager_t time_manager;

/* declare the memory needed for the clients
 * this field tracks which timeouts have triggered
 * for a specific client */
uint32_t *client_state = NULL;

/* Prototype for this function is not generated by the camkes templates yet */
seL4_Word the_timer_get_sender_id();
void the_timer_emit(unsigned int);
int the_timer_largest_badge(void);

static ps_io_ops_t io_ops;

static inline uint64_t current_time_ns()
{
    uint64_t time;
    int error = ltimer_get_time(&ltimer, &time);
    ZF_LOGF_IF(error, "Failed to get time");
    return time;
}

static inline unsigned int get_time_token(int cid, int tid)
{
    return (unsigned int) cid * timers_per_client + tid;
}

static int signal_client(uintptr_t token)
{

    int cid = ((int) token) / timers_per_client;
    int tid = ((int) token) % timers_per_client;

    assert(client_state != NULL);

    client_state[cid] |= BIT(tid);
    the_timer_emit(cid + 1);

    return 0;
}

void time_server_ltimer_handle(UNUSED void *empty_token, ltimer_event_t ltimer_event)
{
    int error = time_server_lock();
    ZF_LOGF_IF(error, "Failed to lock time server");

    error = tm_update(&time_manager);
    ZF_LOGF_IF(error, "Failed to update time manager");

    error = time_server_unlock();
    ZF_LOGF_IF(error, "Failed to unlock time server");
}

static int _oneshot_relative(int cid, int tid, uint64_t ns)
{
    if (tid >= timers_per_client || tid < 0) {
        ZF_LOGE("invalid tid, 0 >= %d >= %d\n", tid, timers_per_client);
        return -1;
    }

    int error = time_server_lock();
    ZF_LOGF_IF(error, "Failed to lock time server");

    unsigned int id = get_time_token(cid, tid);
    error = tm_register_rel_cb(&time_manager, ns, id, signal_client, (uintptr_t) id);
    ZF_LOGF_IF(error, "Failed to set timeout");

    error = time_server_unlock();
    ZF_LOGF_IF(error, "Failed to unlock time server");
    return 0;
}

static int _oneshot_absolute(int cid, int tid, uint64_t ns)
{
    if (tid >= timers_per_client || tid < 0) {
        ZF_LOGE("invalid tid, 0 >= %d >= %d\n", tid, timers_per_client);
        return -1;
    }

    int error = time_server_lock();
    ZF_LOGF_IF(error, "Failed to lock time server");

    unsigned int token = get_time_token(cid, tid);

    error = tm_register_abs_cb(&time_manager, ns, token, signal_client, (uintptr_t) token);
    if (error == ETIME) {
        signal_client(token);
        error = 0;
    }
    ZF_LOGF_IF(error, "Failed to set timeout");

    error = time_server_unlock();
    ZF_LOGF_IF(error, "Failed to unlock time server");
    return 0;
}

static int _periodic(int cid, int tid, uint64_t ns)
{
    if (tid >= timers_per_client || tid < 0) {
        ZF_LOGE("invalid tid, 0 >= %d >= %d\n", tid, timers_per_client);
        return -1;
    }

    int error = time_server_lock();
    ZF_LOGF_IF(error, "Failed to lock time server");

    unsigned int token = get_time_token(cid, tid);
    error = tm_register_periodic_cb(&time_manager, ns, 0, token, signal_client, (uintptr_t) token);
    ZF_LOGF_IF(error, "Failed to set timeout");

    error = time_server_unlock();
    ZF_LOGF_IF(error, "Failed to unlock time server");
    return 0;
}

static int _stop(int cid, int tid)
{
    if (tid >= timers_per_client || tid < 0) {
        ZF_LOGE("invalid tid, 0 >= %d >= %d\n", tid, timers_per_client);
        return -1;
    }
    int error = time_server_lock();
    ZF_LOGF_IF(error, "Failed to lock time server");

    error = tm_deregister_cb(&time_manager, get_time_token(cid, tid));
    ZF_LOGF_IF(error, "Failed to deregister callback");

    error = time_server_unlock();
    ZF_LOGF_IF(error, "Failed to unlock time server");
    return 0;
}

static unsigned int _completed(int cid)
{
    int error = time_server_lock();
    ZF_LOGF_IF(error, "Failed to lock time server");

    assert(client_state != NULL);
    unsigned int ret = client_state[cid];
    client_state[cid] = 0;

    error = time_server_unlock();
    ZF_LOGF_IF(error, "Failed to unlock time server");

    return ret;
}

static uint64_t _time(int cid)
{
    return current_time_ns();
}

/* substract 1 from the badge as we started counting badges at 1
 * to avoid using the 0 badge */
int the_timer_oneshot_relative(int id, uint64_t ns)
{
    return _oneshot_relative(the_timer_get_sender_id() - 1, id, ns);
}

int the_timer_oneshot_absolute(int id, uint64_t ns)
{
    return _oneshot_absolute(the_timer_get_sender_id() - 1, id, ns);
}

int the_timer_periodic(int id, uint64_t ns)
{
    return _periodic(the_timer_get_sender_id() - 1, id, ns);
}

int the_timer_stop(int id)
{
    return _stop(the_timer_get_sender_id() - 1, id);
}

unsigned int the_timer_completed()
{
    return _completed(the_timer_get_sender_id() - 1);
}

uint64_t the_timer_time()
{
    return _time(the_timer_get_sender_id() - 1);
}

void post_init()
{
    int error = time_server_lock();
    ZF_LOGF_IF(error, "Failed to lock timer server");

    error = camkes_io_ops(&io_ops);
    ZF_LOGF_IF(error, "Failed to get camkes_io_ops");

    error = ps_calloc(&(io_ops.malloc_ops), the_timer_largest_badge(), sizeof(*client_state), (void **) &client_state);
    ZF_LOGF_IF(error, "Failed to allocate client state");

    if (plat_pre_init) {
        plat_pre_init();
    }

    error = ltimer_default_init(&ltimer, io_ops, time_server_ltimer_handle, NULL);
    ZF_LOGF_IF(error, "Failed to init timer");

    if (plat_post_init) {
        plat_post_init(&ltimer);
    }

    int num_timers = timers_per_client * the_timer_largest_badge();
    tm_init(&time_manager, &ltimer, &io_ops, num_timers);
    for (unsigned int i = 0; i < num_timers; i++) {
        error = tm_alloc_id_at(&time_manager, i);
        ZF_LOGF_IF(error, "Failed to alloc id at %u\n", i);
    }

    error = time_server_unlock();
    ZF_LOGF_IF(error, "Failed to unlock timer server");

    set_putchar(putchar_putchar);
}
